function gravy_validator(form){
	var me=this;
	this.fieldNames=[];
	this.fields={};
	this.form=form;
	this.errors=[];
	this.init=function(form){
		me.form=(form==null)?me.form:form;
		$(me.form).submit(me.validateAllFields);
	}

	this.addField=function(fieldName,displayName,inputType){
		me.fieldNames.push(fieldName);
		me.fields[fieldName]={"fieldName":fieldName,"displayName":displayName,"inputType":inputType,"validate":new Array()};
		if(inputType.toLowerCase()!="hidden"&&inputType.toLowerCase()!="checkbox"){
			$("#"+fieldName).change(me.validateFieldOnEvent);
			$("#"+fieldName).blur(me.validateFieldOnEvent);
		}else if(inputType.toLowerCase()=="checkbox"){
			$("#"+fieldName).click(me.validateFieldOnEvent);
		}
	}
	
	this.addRule=function(field,type,opts){
		me.fields[field].validate.push({"type":type,"options":opts});
	}
	
	this.addError=function(fieldName,message){
		me.errors.push({"fieldName":fieldName,"message":message});
	}
	
	this.showError=function(fieldName,message,showError){
		showError=(showError==null)?true:showError;
		message=(message==null)?"There is an error in the \""+fieldName+"\" field.":message;
		if(showError){
			message=message.replace("%displayName%",me.fields[fieldName].displayName);
			$("#"+fieldName).html(message);
			$("#"+fieldName).show();
		}else{
			me.addError(fieldName,message);
		}
	}
	this.hideError=function(fieldName){
		$("#"+fieldName).hide();
	}
	this.getTargetFromEvent=function(e){
		if(e){
			return(e.target);
		}else if(event.srcElement){
			return(event.srcElement);
		}
	}
	this.validateFieldOnEvent=function(){
		me.validateField(this,true);
	}
	this.validateField=function(field,showError){
		showError=(showError==null)?false:showError;
		me.hideError(field.name);
		var data=me.fields[field.name];
		var fn;
		for(var c=0;c<data.validate.length;c++){
			fn=data.validate[c].type;
			opts=data.validate[c].options;
			if(me[fn]){
				if(!me[fn](field,data.displayName,data.inputType,showError,opts)){
					break;
				}
			}
		}
	}
	this.showAllErrors=function(){
		var error;
		for(var c=0;c<me.errors.length;c++){
			error=me.errors[c];
			me.showError(error.fieldName,error.message);
		}
	}
	this.focusFirstErrorField=function(){
		$("#"+me.errors[0].fieldName).focus();
	}
	this.isDate=function(date){
		//alert("Checking Date: "+date);
		var result=false;
		var ar=date.split(" ");
		var d;
		var time="";
		if(ar.length==2){
			date=ar[0];
			time=ar[1];
		}else if(ar.length==1){
			date=ar[0];
		}
		//alert("Date: "+date);
		//alert("Time: "+time);
		if(date.length>0){
			//alert("date.length>0");
			date=date.replace(/[\-/]/g,',');
			date=date.split(',');
			if(time.length>0){
				//alert("time.length>0");
				time=time.split(':');
			}
			if(date.length==3&&time.length==3){
				//alert("date.length==3&&time.length==3");
				d=new Date(date[0],date[1],date[2],time[0],time[1],time[2]);
				if(1*date[2]==d.getDate()&&1*date[1]==(d.getMonth())&&1*date[0]==d.getFullYear()&&1*time[0]==d.getHours()&&1*time[1]==d.getMinutes()&&1*time[2]==d.getSeconds()){
					result=true;
				}else{
					//alert((1*date[0])+"=="+d.getDate()+" && "+(1*date[1])+"=="+(d.getMonth())+" && "+(1*date[3])+"=="+d.getFullYear()+" && "+(1*time[0])+"=="+d.getHours()+" && "+(1*time[1])+"=="+d.getMinutes()+" && "+(1*time[2])+"=="+d.getSeconds());
				}
			}else if(date.length==3&&time.length==2){
				d=new Date(date[0],date[1],date[2],time[0],time[1]);
				//alert("date.length==3&&time.length==2");
				if(1*date[2]==d.getDate()&&1*date[1]==(d.getMonth())&&1*date[0]==d.getFullYear()&&1*time[0]==d.getHours()&&1*time[1]==d.getMinutes()){
					result=true;
				}
			}else if(date.length==3){
				//alert("date.length==3");
				d=new Date(date[0],date[1],date[2]);
				if(1*date[2]==d.getDate()&&1*date[1]==(d.getMonth())&&1*date[0]==d.getFullYear()){
					result=true;
				}
			}
		}
		//alert("Date Valid: "+result);
		return(result);
	}
	this.validateAllFields=function(e){
		var result=false;
		var field;
		me.errors=[];
		for(var c=0;c<me.fieldNames.length;c++){
			me.validateField($("#"+me.fieldNames[c])[0]);
		}
		if(me.errors.length>0){
			me.showAllErrors();
			me.focusFirstErrorField();
		}else{
			result=true;
		}
		return(result);
	}
	
	
	this.required=function(field,displayName,inputType,showError,opts){
		showError=(showError==null)?true:showError;
		var result=true;
		switch(inputType.toLowerCase()){
			case "text":
			case "file":
				is_required=(opts.value==null)?true:opts.value;
				error_msg=(opts.errorMessage==null)?'A value must be entered in the "'+displayName+'" field.':opts.errorMessage;
				if(field.value.length<1&&is_required){
					me.showError(field.name,error_msg,showError);
					result=false;
				}
				break;
			case "checkbox":
				is_required=(opts.value==null)?true:opts.value;
				error_msg=(opts.errorMessage==null)?'You must check the box labelled "'+displayName+'".':opts.errorMessage;
				if(!field.checked&&is_required){
					me.showError(field.name,error_msg,showError);
					result=false;
				}
				break;
			case "select":
				is_required=(opts.value==null)?true:opts.value;
				null_index=(opts.nullIndex==null)?0:opts.nullIndex;
				error_msg=(opts.errorMessage==null)?'You must select an option in the "'+displayName+'" drop down.':opts.errorMessage;
				if((field.selectedIndex==null_index||field.value.length<1)&&is_required){
					me.showError(field.name,error_msg,showError);
					result=false;
				}
				break;
		}
		return(result);
	}
	this.minimumLength=function(field,displayName,inputType,showError,opts){
		showError=(showError==null)?true:showError;
		var result=true;
		switch(inputType.toLowerCase()){
			case "text":
				min_length=(opts.value==null)?0:opts.value;
				error_msg=(opts.errorMessage==null)?'There must be more than '+min_length+' characters entered in the "'+displayName+'" field.':opts.errorMessage;
				if(field.value.length<min_length&&field.value.length>0){
					result=false;
				}
				break;
		}
		if(!result){
			me.showError(field.name,error_msg,showError);
		}
		return(result);
	}
	this.maximumLength=function(field,displayName,inputType,showError,opts){
		showError=(showError==null)?true:showError;
		var result=true;
		switch(inputType.toLowerCase()){
			case "text":
				max_length=(opts.value==null)?1024:opts.value;
				error_msg=(opts.errorMessage==null)?'There must be less than '+max_length+' characters entered in the "'+displayName+'" field.':opts.errorMessage;
				if(field.value.length>max_length){
					result=false;
				}
				break;
		}
		if(!result){
			me.showError(field.name,error_msg,showError);
		}
		return(result);
	}
	this.equalsField=function(field,displayName,inputType,showError,opts){
		showError=(showError==null)?true:showError;
		var result=true;
		switch(inputType.toLowerCase()){
			case "text":
				field2=document[me.form][opts.value];
				if(field2){
					field2DisplayName=me.fields[field2.name].displayName
					error_msg=(opts.errorMessage==null)?'The value in '+displayName+' must match the value in the "'+field2DisplayName+'" field.':opts.errorMessage;
					if(field.value!=field2.value){
						result=false;
					}
				}
				break;
		}
		if(!result){
			me.showError(field.name,error_msg,showError);
		}
		return(result);
	}
	this.telephone=function(field,displayName,inputType,showError,opts){
		showError=(showError==null)?true:showError;
		var result=true;
		var invalid=/[^0-9\.\(\)\-\+\s]/gi;
		switch(inputType.toLowerCase()){
			case "text":
				replaceInvalidChars=(opts.replaceInvalidChars==null)?false:opts.replaceInvalidChars;
				error_msg=(opts.errorMessage==null)?'Please enter a valid telephone number.':opts.errorMessage;
				
				if(invalid.test(field.value)){
					//alert("There is an error");
					if(replaceInvalidChars){
						field.value=field.value.replace(invalid,"");
					}
					result=false;
				}
				break;
		}
		if(!result){
			me.showError(field.name,error_msg,showError);
		}
		return(result);
	}
	this.email=function(field,displayName,inputType,showError,opts){
		showError=(showError==null)?true:showError;
		var result=true;
		var isEmailValid=/^[\w-\.]{1,}\@([\da-zA-Z-]{1,}\.){1,}[\da-zA-Z-]{2,8}$/gi;
		switch(inputType.toLowerCase()){
			case "text":
				if(field.value.length>0){
					if(!isEmailValid.test(field.value)&&isEmailValid.test(field.value)===false){
						error_msg=(opts.errorMessage==null)?'Please enter a valid email address.':opts.errorMessage;
						result=false;
					}
				}
				break;
		}
		if(!result){
			me.showError(field.name,error_msg,showError);
		}
		return(result);
	}
	this.fixedLength=function(field,displayName,inputType,showError,opts){
		showError=(showError==null)?true:showError;
		var result=true;
		var val_minus_spaces=field.value.replace(/\s/gi,"");
		var len=opts.value;
		switch(inputType.toLowerCase()){
			case "text":
				if(field.value.length>0){
					error_msg=(opts.errorMessage==null)?'The '+displayName+' must be '+len+' characters long.':opts.errorMessage;
					if(val_minus_spaces.length!=len){
						result=false;
					}
				}
				break;
		}
		if(!result){
			me.showError(field.name,error_msg,showError);
		}
		return(result);
	}
	this.numeric=function(field,displayName,inputType,showError,opts){
		showError=(showError==null)?true:showError;
		var result=true;
		var must_be_numeric=opts.value;
		switch(inputType.toLowerCase()){
			case "text":
				field.value=field.value.split(" ").join("");
				if(must_be_numeric&&isNaN(field.value)){
					error_msg=(opts.errorMessage==null)?'The '+displayName+' can only contain numbers.':opts.errorMessage;
					result=false;
				}else if(!must_be_numeric&&!isNaN(field.value)){
					error_msg=(opts.errorMessage==null)?'The '+displayName+' must not contain numbers.':opts.errorMessage;
					result=false;
				}
				break;
		}
		if(!result){
			me.showError(field.name,error_msg,showError);
		}
		return(result);
	}
	this.currency=function(field,displayName,inputType,showError,opts){
		showError=(showError==null)?true:showError;
		var result=true;
		var symbol=(opts.symbol==null)?"$":opts.symbol;
		field.value=field.value.replace(/[^\.\d]/gi,"");
		switch(inputType.toLowerCase()){
			case "text":
				if(field.value.length>0){
					if(isNaN(field.value)){
						error_msg=(opts.errorMessage==null)?'Please enter a valid '+displayName+'.':opts.errorMessage;
						result=false;
					}else{
						var dp=(opts.dp==null)?0:opts.dp;
						var format=(opts.format==null)?false:opts.format;
						field.value=symbol+gravy_round(field.value,dp,format);
					}
				}
				break;
		}
		if(!result){
			me.showError(field.name,error_msg,showError);
		}
		return(result);
	}
	this.url=function(field,displayName,inputType,showError,opts){
		showError=(showError==null)?true:showError;
		var result=true;
		var valid=/^(https?|ftp)\:\/\/[a-z0-9+\$_-]+((\.[a-z0-9+\$_-]+)+)*/gi;
		switch(inputType.toLowerCase()){
			case "text":
				if(field.value.length>0){
					if(field.value.substr(0,6).toLowerCase()!="ftp://"&&field.value.substr(0,7).toLowerCase()!="http://"&&field.value.substr(0,8).toLowerCase()!="https://"){
						field.value="http://"+field.value;
					}
					if(!valid.test(field.value)&&valid.test(field.value)===false){
						error_msg=(opts.errorMessage==null)?'Please enter a valid '+displayName+'.':opts.errorMessage;
						result=false;
					}
				}
				break;
		}
		if(!result){
			me.showError(field.name,error_msg,showError);
		}
		return(result);
	}
	this.fileType=function(field,displayName,inputType,showError,opts){
		showError=(showError==null)?true:showError;
		var result=true;
		switch(inputType.toLowerCase()){
			case "file":
				if(field.value.length>0){
					//alert("OK"+field.value);
					var fileTypes=opts.value.split(",");
					var ft="";
					if(field.value.lastIndexOf(".")>-1){
						ft=field.value.substr(field.value.lastIndexOf(".")+1);
					}
					if(fileTypes.length>0){
						var found=false;
						for(var c=0;c<fileTypes.length;c++){
							if(fileTypes[c].toLowerCase()==ft.toLowerCase()){
								found=true;
								break;
							}
						}
						if(!found){
							error_msg=(opts.errorMessage==null)?"The "+displayName+" file must be one of the following file formats "+opts.value+".":opts.errorMessage;
							result=false;
						}
					}
				}
				break;
		}
		if(!result){
			me.showError(field.name,error_msg,showError);
		}
		return(result);
	}
	this.fileSize=function(field,displayName,inputType,showError,opts){
		return(true);
	}
	this.date=function(field,displayName,inputType,showError,opts){
		showError=(showError==null)?true:showError;
		var result=true;
		switch(inputType.toLowerCase()){
			case "text":
			case "hidden":
				//alert("Checking date");
				if(!me.isDate(field.value)){
					error_msg=(opts.errorMessage==null)?'The '+displayName+' is not a valid date.':opts.errorMessage;
					result=false;
				}
				break;
		}
		if(!result){
			me.showError(field.name,error_msg,showError);
		}
		return(result);
	}
}